﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using CrawlerFramework.ConfigEntity;
using CrawlerFramework.Models;
using CrawlerFramework.PageProcessor;
using CrawlerFramework.Repository;
using log4net;
using MediaBot.Library;

namespace CrawlerFramework
{
    public class CrawlerEngine
    {
        #region Fields

        private readonly ILogService _log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
        private readonly string _crawlerConfigFile;
        private readonly IPageProcessorBuilder _pageProcessorFactory;

        #endregion


        #region Private methods

        private IEnumerable<LinkConfig> GetAllLinkConfigs(string path)
        {
            try
            {
                var xmlSerialzier = new XmlSerializer<CrawlerConfig>();
                var config = xmlSerialzier.Deserialize(path);

                return config.Links.Cast<LinkConfig>().ToList();
            }
            catch (Exception e)
            {
                _log.Error("GetAllLinkConfigsException - Path: " + path, e);
            }

            return null;
        }

        private void CheckForNewScheduler()
        {
            var schedulerRepo = new ScheduledJobRepository();
            var processTrackingRepo = new ProcessTrackingRepository();

            _log.Info("Start checking for new scheduler");

            try
            {
                var schedulers =
                    schedulerRepo.All.ToList()
                        .Where(s => !processTrackingRepo.All.Any(p => p.LinkConfigGuid == s.LinkConfigGuid));

                foreach (var scheduler in schedulers)
                {
                    _log.Info("Insert new scheduler - GUID: " + scheduler.LinkConfigGuid);

                    var newProcessTrack = new ProcessTracking
                    {
                        LinkConfigGuid = scheduler.LinkConfigGuid,
                        CrawlStatus = (int) CrawlStatus.Initial
                    };
                    processTrackingRepo.AddOrUpdate(newProcessTrack);
                }

                processTrackingRepo.Save();
            }
            catch (Exception e)
            {
                _log.Error("CheckForNewScheduler Exception", e);
            }
            finally
            {
                schedulerRepo.Dispose();
                processTrackingRepo.Dispose();
            }
        }

        private static List<Link> GetAllLinksMatchLinkConfigPattern(LinkConfig linkConfig)
        {
            using (var linkRepo = new LinkRepository())
            {
                var allLinks = linkRepo.FindBy(l => !l.IsDelete).ToList();
                return allLinks.Where(l => linkConfig.IsSatisfy(l.Url)).ToList();
            }
        }

        private void ExecuteProcess(ProcessTracking process)
        {
            var processTrackingRepo = new ProcessTrackingRepository();
            var linkRepo = new LinkRepository();
            int successCount = 0, errorCount = 0;

            var isParallel = ConfigurationSettings.Settings.IsRunParallelOnSite;
            var numberOfThread = ConfigurationSettings.Settings.NumberOfThreadsInPool;

            try
            {
                var linkConfigs = GetAllLinkConfigs(_crawlerConfigFile);

                process.CrawlStatus = CrawlStatus.Processing;
                process.LastRunningDate = DateTime.Now;
                processTrackingRepo.AddOrUpdate(process);
                processTrackingRepo.Save();

                var linkConfig = linkConfigs.FirstOrDefault(l => l.Guid == Guid.Parse(process.LinkConfigGuid));
                if (linkConfig == null)
                    throw new InvalidOperationException("There is no config for GUID: " + process.LinkConfigGuid);

                var links = GetAllLinksMatchLinkConfigPattern(linkConfig);

                #region Run parallel

                if (isParallel)
                {
                    var option = new ParallelOptions {MaxDegreeOfParallelism = numberOfThread};
                    var syncObject = new Object();


                    Parallel.ForEach(links, option, link =>
                    {
                        var isComplete = true;
                        if (ShouldProcessThisLink(link))
                        {
                            link.BeginExecution();
                            var processor = _pageProcessorFactory.GetPageProcessor(linkConfig, link.LinkId);
                            isComplete = processor.Execute();
                            link.EndExecution(isComplete);
                        }

                        lock (syncObject)
                        {
                            if (isComplete) successCount += 1;
                            else errorCount += 1;
                            var total = successCount + errorCount;
                            if (total >= links.Count)
                                OnProcessExecuteCompleted(process.ProcessTrackingId, successCount, errorCount);
                        }
                        
                    });

                }
                else
                {
                    foreach (var link in links)
                    {
                        if (!ShouldProcessThisLink(link))
                        {
                            successCount += 1;
                            continue;
                        }

                        link.BeginExecution();
                        var processor = _pageProcessorFactory.GetPageProcessor(linkConfig, link.LinkId);
                        var isComplete = processor.Execute();
                        link.EndExecution(isComplete);

                        if (isComplete)
                            successCount += 1;
                        else
                            errorCount += 1;
                    }

                    OnProcessExecuteCompleted(process.ProcessTrackingId, successCount, errorCount);
                }

                #endregion
            }
            catch (Exception e)
            {
                _log.Error("ExecuteProcess: " + process.ProcessTrackingId + " exception", e);
            }
            finally
            {
                processTrackingRepo.Dispose();
                linkRepo.Dispose();
            }
        }

        private void OnProcessExecuteCompleted(int processId, int successCount, int errorCount)
        {
            var scheduleRepo = new ScheduledJobRepository();
            var historyRepo = new BaseRepository<CrawlHistory>();
            var trackingRepo = new ProcessTrackingRepository();
            try
            {
                var processTracking = trackingRepo.FindBy(p => p.ProcessTrackingId == processId).FirstOrDefault();
                if (processTracking != null)
                {
                    processTracking.CrawlStatus = CrawlStatus.Completed;

                    var scheduler =
                        scheduleRepo.FindBy(s => s.LinkConfigGuid == processTracking.LinkConfigGuid).FirstOrDefault();
                    if (scheduler != null && scheduler.Days > 0)
                    {
                        if (processTracking.LastRunningDate != null)
                        {
                            var nextRunningDate = processTracking.LastRunningDate.Value.AddDays(scheduler.Days);
                            processTracking.NextRunningDate = nextRunningDate;
                        }
                    }

                    trackingRepo.AddOrUpdate(processTracking);
                    trackingRepo.Save();


                    var history = new CrawlHistory
                    {
                        CrawlDate = processTracking.LastRunningDate.Value,
                        SuccessCount = successCount,
                        ErrorCount = errorCount,
                        ProcessTrackingId = processId
                    };

                    historyRepo.Add(history);
                }
                historyRepo.Save();
            }
            catch (Exception e)
            {
                _log.Error("On Process Update Completed Process Exception. ProcessId: " + processId, e);
            }
            finally
            {
                trackingRepo.Dispose();
                historyRepo.Dispose();
                scheduleRepo.Dispose();
            }
        }

        private static bool ShouldProcessThisLink(Link link)
        {
            return !link.CrawlDate.HasValue || link.CrawlDate.Value.Date != DateTime.Now.Date ||
                   link.CrawlStatus != CrawlStatus.Completed;
        }

        #endregion



        #region Public methods

        /// <summary>
        /// Execute newly added crawling process
        /// </summary>
        public void ExecuteNewScheduler()
        {
            var processRepo = new ProcessTrackingRepository();

            var isParallel = ConfigurationSettings.Settings.IsRunParallelOnSite;

            try
            {
                CheckForNewScheduler();

                var firstRunProcesses =
                    processRepo.FindBy(p => p.LastRunningDate == null && p.IsDelete == false).ToList();

                if (isParallel)
                {
                    Parallel.ForEach(firstRunProcesses, ExecuteProcess);
                }
                else
                    foreach (var process in firstRunProcesses)
                    {
                        ExecuteProcess(process);
                    }
            }
            catch (Exception e)
            {
                _log.Error("CrawlerEngine.ExecuteNewScheduler() exception", e);
            }
            finally
            {
                processRepo.Dispose();
            }
        }

        /// <summary>
        /// Execute all crawling process that is scheduled to run this day
        /// </summary>
        public void ExecuteScheduledProcess()
        {
            var processRepo = new ProcessTrackingRepository();
            var isParallel = ConfigurationSettings.Settings.IsRunParallelOnSite;

            try
            {
                
                var dt = DateTime.Now;
                var resumeProcess = processRepo.FindBy(p => p.CrawlStatus == CrawlStatus.Processing && !p.IsDelete);
                var onThisDayScheduledProcesses =
                    processRepo.FindBy(
                        p =>
                            p.NextRunningDate.HasValue && p.NextRunningDate.Value.Day == dt.Day &&
                            p.NextRunningDate.Value.Month == dt.Month && p.NextRunningDate.Value.Year == dt.Year
                            && p.CrawlStatus == CrawlStatus.Completed && !p.IsDelete);

                var processes = new List<ProcessTracking>(resumeProcess);
                processes.AddRange(onThisDayScheduledProcesses);

                if (isParallel)
                {
                    Parallel.ForEach(processes, ExecuteProcess);
                }
                else
                    foreach (var process in processes)
                    {
                        ExecuteProcess(process);
                    }
            }
            catch (Exception e)
            {
                _log.Error("CrawlerEngine.ExecuteScheduledProcess() exception", e);
            }
            finally
            {
                processRepo.Dispose();
            }

        }

        /// <summary>
        /// Execute a specified crawling process based on input process GUID
        /// </summary>
        /// <param name="processGuid"></param>
        public void ExecuteProcessTracking(Guid processGuid)
        {
            var processTrackingRepo = new ProcessTrackingRepository();

            try
            {
                var process =
                    processTrackingRepo.FindBy(p => Guid.Parse(p.LinkConfigGuid) == processGuid && !p.IsDelete)
                        .FirstOrDefault();
                if (process != null)
                    ExecuteProcess(process);
            }
            catch (Exception e)
            {
                _log.Error("ExecuteProcessTracking Exception - GUID: " + processGuid, e);
            }
            finally
            {
                processTrackingRepo.Dispose();
            }
        }

        #endregion



        #region Constructor

        public CrawlerEngine(string crawlerConfigFile, IPageProcessorBuilder pageProcessorFactory)
        {
            _crawlerConfigFile = crawlerConfigFile;
            _pageProcessorFactory = pageProcessorFactory;
        }

        #endregion





        


    }
}
